<?php

namespace mongrove;

use \MongoId;
use \MongoDB;

/**
 * The Collection class is a representation for a collection of Records of
 * a given type. Several Collections for different Records can still store the
 * Records in the same Mongo collection, in order to improve generic query
 * possibilities.
 *
 * @author Gido Hakvoort <gido@hakvoort.it>
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 *
 */
class Collection {

    /*
     * TODO: add caching for find and findOne
     */


    protected $db = null;
    protected $type = null;
    protected $collection = null;
    protected $blueprint = null;

    protected $records = array();

    /**
     * @var CollectionQueryService
     */
    protected $queryService;

    /**
     * Construct a new Collection of the given type, stored in the
     * given MongoDB in the given collection.
     *
     * @param string $type The type of Record contained in the Collection
     * @param string $collection The name of the Mongo collection in which the Records are stored
     * @param \MongoDB $db The Mongo database handle which holds the named collection
     */
    public function __construct($type, $collection, \MongoDB $db = null) {
        $this->type = $type;
        $this->collection = $collection;
        $this->db = $db;

        $this->blueprint = $type :: getStructure();
        $this->queryService = new CollectionQueryService($this->collection, $this->db);
    }

    /**
     * Overriding the __call method is intended for simple findBy* and findOneBy*
     * functionality.
     *
     * @param $name
     * @param $arguments
     *
     * @throws \Exception In case an invalid method is executed
     *
     * @return Record|QueryResult
     */
    public function __call($name, $arguments) {
        if(substr($name, 0, 6) === 'findBy') {
            $method = 'findBy';
            $field = strtolower(substr($name, 6, strlen($name)));
        } else if(substr($name, 0, 9) === 'findOneBy') {
            $method = 'findOneBy';
            $field = strtolower(substr($name, 9, strlen($name)));
        } else {
            throw new \Exception("Unknown method: '{$name}' ");
        }

        if(!isset($arguments[0])) {
            throw new \Exception("You must specify a value for '{$method}'.");
        }

        if(!$this->blueprint->hasField($field)) {
            throw new \Exception("Field: '{$field}' does not exist for '{$this->type}'.");
        }

        return $this->$method($field, $arguments[0]);
    }

    /**
     * Return the backing Mongo database for this collection.
     *
     * @return \MongoDB
     */
    public function getDatabase() {
        return $this->db;
    }

    /**
     * Create a new Query which operates on the contained type.
     *
     * @return \mongrove\Query
     */
    public function createQuery() {
        return new Query($this->type, $this->blueprint, $this->queryService);
    }

    /**
     * Drop the collection from the database.
     *
     * @return boolean
     */
    public function drop() {
        return $this->db->__get($this->collection)->drop();
    }

    /**
     * Count the documents contained in this collection.
     *
     * @return int
     */
    public function size() {
        return $this->db->__get($this->collection)->count();
    }

    /**
     * Get all elements of this or any underlying subtype with lie
     * in the same Mongo collection.
     *
     * @return \mongrove\QueryResult
     */
    public function findAll() {
        if($this->db === null) {
            return null;
        }

        return $this->createQuery()->execute();
    }

    /**
     * Find all elements of the contained type where the given field
     * matches the given value.
     *
     * @param string $field The name of the field for which the match check is performed
     * @param mixed $value The value against the field is matched
     *
     * @return \mongrove\QueryResult
     */
    public function findBy($field, $value) {
        if($this->db === null) {
            return;
        }

        return $this->createQuery()
        ->whereOperator($field, Command :: CON_OP_EQUAL, $value)
        ->execute();
    }

    /**
     * Find one value of the contained type where the given field
     * matches the given value.
     *
     * @param string $field The name of the field for which to search
     * @param mixed $value The value against the field is matched
     *
     * @return \mongrove\Record
     */
    public function findOneBy($field, $value) {
        if($this->db === null) {
            return;
        }

        $result = $this->createQuery()
                       ->whereOperator($field, Command :: CON_OP_EQUAL, $value)
                       ->limit(1)
                       ->execute();

        $result->rewind();

        if($result->valid()) {
            return $result->current();
        }

        return null;
    }

    /**
     * Find the Record of the contained type with the given id.
     *
     * @param string $value The id for which the Record needs to be retrieved.
     *
     * @return Record
     */
    public function findOneById($value) {
        return $this->findOneBy(Constant :: ID, new MongoId($value));
    }

    /**
     * Delete a Record from the Collection
     *
     * @param Record $record
     */
    public function delete(Record $record) {
        $id = $record->getId();

        if(!isset($id)) {
            return false;
        }

        // TODO mark record as deleted

        return $this->db
                    ->__get($this->collection)
                    ->remove(array(Constant :: ID => new MongoId($id)));
    }

    /**
     * Save a Record in the Collection.
     *
     * @param Record $record The record to be stored in this Collection
     *
     * @return boolean True if the Record was successfully stored
     */
    public function save(Record $record) {
        if($this->db === null) {
            return false;
        }

        if($record->isDeleted()) {
            throw new \Exception("Object was deleted and could not be saved.");
        }

        $id = $record->getId();

        if($id === null) {
            $data = $record->dehydrate();
            $data[Constant :: TYPES] = $record :: getTypeHierarchy();

            $success = $this->db
                            ->__get($this->collection)
                            ->insert($data);

            $record->setId((string)$data[Constant :: ID]);
        } else {
            $data = $record->getMutations();

            $id = new MongoId($id);

            $collection = $this->db->__get($this->collection);

            $success = true;

            /*
             * Insert multiple mutations
             */
            foreach($data as $mutationCycle) {
                if(count($mutationCycle) > 0) {
                    $success = $collection->update(array(Constant :: ID => $id), $mutationCycle) && $success;
                }
            }
        }

        if($success) {
            $record->clean();
        }

        return $success;
    }
}

/**
 *
 * The CollectionQueryService is a service capable of creating find and findOne queries.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @author Gido Hakvoort <gido@hakvoort.it>
 *
 */
class CollectionQueryService {

    protected $db;
    protected $collection;

    /**
     *
     * @param string $collection The name of the collection
     * @param MongoDB $db The handle to the Mongo database
     */
    public function __construct($collection, MongoDB $db = null) {
        $this->db = $db;
        $this->collection = $collection;
    }

    /**
     * Create a find query.
     *
     * @return \MongoCursor
     */
    public function find() {
        return $this->db->__get($this->collection)->find();
    }

    /**
     * FIXME
     *
     * @return array The MongoCollection findOne result
     */
    public function findOne(array $query = array(), array $fields = array()) {
        throw new \Exception('Not implemented yet');

        return $this->db->__get($this->collection)->findOne();
    }

}